/*-------------------------------------------------------------------------
 *
 * nodeLockRows.c
 *	  处理FOR UPDATE/FOR SHARE行锁定的例程
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/executor/nodeLockRows.c
 *
 *-------------------------------------------------------------------------
 */
/*
 * 接口例程
 *		ExecLockRows		- 获取锁定的行
 *		ExecInitLockRows	- 初始化节点和子节点..
 *		ExecEndLockRows		- 关闭节点和子节点
 */

#include "postgres.h"

#include "access/tableam.h"
#include "access/xact.h"
#include "executor/executor.h"
#include "executor/nodeLockRows.h"
#include "foreign/fdwapi.h"
#include "miscadmin.h"
#include "utils/rel.h"


/* ----------------------------------------------------------------
 *		ExecLockRows
 * ----------------------------------------------------------------
 */
static TupleTableSlot *			/* 返回：一个元组或 NULL */
fc_ExecLockRows(PlanState *fc_pstate)
{
	LockRowsState *fc_node = castNode(LockRowsState, fc_pstate);
	TupleTableSlot *fc_slot;
	EState	   *fc_estate;
	PlanState  *fc_outerPlan;
	bool		fc_epq_needed;
	ListCell   *fc_lc;

	CHECK_FOR_INTERRUPTS();

	/*
	 * 从节点获取信息
	 */
	fc_estate = fc_node->ps.state;
	fc_outerPlan = outerPlanState(fc_node);

	/*
	 * 从子计划获取下一个元组（如果有）。
	 */
lnext:
	fc_slot = ExecProcNode(fc_outerPlan);

	if (TupIsNull(fc_slot))
	{
		/* 在退出之前释放 EPQ 机制持有的任何资源 */
		EvalPlanQualEnd(&fc_node->lr_epqstate);
		return NULL;
	}

	/* 除非我们获取更新的元组版本，否则不需要 EvalPlanQual */
	fc_epq_needed = false;

	/*
	 * 尝试锁定源元组。 （注意，我们只在 lr_arowMarks 中有锁定行标记。）
	 */
	foreach(fc_lc, fc_node->lr_arowMarks)
	{
		ExecAuxRowMark *fc_aerm = (ExecAuxRowMark *) lfirst(fc_lc);
		ExecRowMark *fc_erm = fc_aerm->rowmark;
		Datum		fc_datum;
		bool		fc_isNull;
		ItemPointerData fc_tid;
		TM_FailureData fc_tmfd;
		LockTupleMode fc_lockmode;
		int			fc_lockflags = 0;
		TM_Result	fc_test;
		TupleTableSlot *fc_markSlot;

		/* 清除此关系的任何剩余测试元组 */
		fc_markSlot = EvalPlanQualSlot(&fc_node->lr_epqstate, fc_erm->relation, fc_erm->rti);
		ExecClearTuple(fc_markSlot);

		/* 如果是子关系，必须检查是否产生了这一行 */
		if (fc_erm->rti != fc_erm->prti)
		{
			Oid			fc_tableoid;

			fc_datum = ExecGetJunkAttribute(fc_slot,
										 fc_aerm->toidAttNo,
										 &fc_isNull);
			/* 不应该得到空结果…… */
			if (fc_isNull)
				elog(ERROR, "tableoid is NULL");
			fc_tableoid = DatumGetObjectId(fc_datum);

			Assert(OidIsValid(fc_erm->relid));
			if (fc_tableoid != fc_erm->relid)
			{
				/* 这个子项目前处于非活动状态 */
				fc_erm->ermActive = false;
				ItemPointerSetInvalid(&(fc_erm->curCtid));
				ExecClearTuple(fc_markSlot);
				continue;
			}
		}
		fc_erm->ermActive = true;

		/* 获取元组的 ctid */
		fc_datum = ExecGetJunkAttribute(fc_slot,
									 fc_aerm->ctidAttNo,
									 &fc_isNull);
		/* 不应该得到空结果…… */
		if (fc_isNull)
			elog(ERROR, "ctid is NULL");

		/* 对于外部表的请求必须传递给其 FDW */
		if (fc_erm->relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
		{
			FdwRoutine *fc_fdwroutine;
			bool		fc_updated = false;

			fc_fdwroutine = GetFdwRoutineForRelation(fc_erm->relation, false);
			/* 这应该已经检查过了，但为了安全起见还是检查一下 */
			if (fc_fdwroutine->RefetchForeignRow == NULL)
				ereport(ERROR,
						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						 errmsg("cannot lock rows in foreign table \"%s\"",
								RelationGetRelationName(fc_erm->relation))));

			fc_fdwroutine->RefetchForeignRow(fc_estate,
										  fc_erm,
										  fc_datum,
										  fc_markSlot,
										  &fc_updated);
			if (TupIsNull(fc_markSlot))
			{
				/* 无法获取锁，因此跳过此行 */
				goto lnext;
			}

			/*
			 * 如果 FDW 说元组在获取锁之前已被更新，我们需要
			 * 执行 EPQ 测试，以查看资格是否仍然满足
			 */
			if (fc_updated)
				fc_epq_needed = true;

			continue;
		}

		/* 好吧，尝试锁定（并获取）元组 */
		fc_tid = *((ItemPointer) DatumGetPointer(fc_datum));
		switch (fc_erm->markType)
		{
			case ROW_MARK_EXCLUSIVE:
				fc_lockmode = LockTupleExclusive;
				break;
			case ROW_MARK_NOKEYEXCLUSIVE:
				fc_lockmode = LockTupleNoKeyExclusive;
				break;
			case ROW_MARK_SHARE:
				fc_lockmode = LockTupleShare;
				break;
			case ROW_MARK_KEYSHARE:
				fc_lockmode = LockTupleKeyShare;
				break;
			default:
				elog(ERROR, "unsupported rowmark type");
				fc_lockmode = LockTupleNoKeyExclusive; /* 保持编译器安静 */
				break;
		}

		fc_lockflags = TUPLE_LOCK_FLAG_LOCK_UPDATE_IN_PROGRESS;
		if (!IsolationUsesXactSnapshot())
			fc_lockflags |= TUPLE_LOCK_FLAG_FIND_LAST_VERSION;

		fc_test = table_tuple_lock(fc_erm->relation, &fc_tid, fc_estate->es_snapshot,
								fc_markSlot, fc_estate->es_output_cid,
								fc_lockmode, fc_erm->waitPolicy,
								fc_lockflags,
								&fc_tmfd);

		switch (fc_test)
		{
			case TM_WouldBlock:
				/* 在 SKIP LOCKED 模式下无法锁定元组 */
				goto lnext;

			case TM_SelfModified:

				/*
				 * 目标元组已被当前命令或当前事务中的后续命令更新或删除。
				 * 在前一种情况下，我们 *必须* 忽略该元组，以避免“万圣节问题”
				 * 的重复更新尝试。在后一种情况下，获取更新的元组可能是合理的，
				 * 但这样做需要更改 heap_update 和 heap_delete，不要
				 * 抱怨更新“不可见”的元组，这看起来相当可怕
				 * （table_tuple_lock 不会抱怨，但很少有调用者期望 TM_Invisible，
				 * 而我们也不期望）。所以目前，将元组视为已删除，并且不处理。
				 */
				goto lnext;

			case TM_Ok:

				/*
				 * 成功获取锁，锁定的元组保存在
				 * markSlot 以备需要时进行 EvalPlanQual 测试。
				 */
				if (fc_tmfd.traversed)
					fc_epq_needed = true;
				break;

			case TM_Updated:
				if (IsolationUsesXactSnapshot())
					ereport(ERROR,
							(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
							 errmsg("could not serialize access due to concurrent update")));
				elog(ERROR, "unexpected table_tuple_lock status: %u",
					 fc_test);
				break;

			case TM_Deleted:
				if (IsolationUsesXactSnapshot())
					ereport(ERROR,
							(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
							 errmsg("could not serialize access due to concurrent update")));
				/* 元组已删除，因此不要返回它 */
				goto lnext;

			case TM_Invisible:
				elog(ERROR, "attempted to lock invisible tuple");
				break;

			default:
				elog(ERROR, "unrecognized table_tuple_lock status: %u",
					 fc_test);
		}

		/* 记住锁定元组的 TID 以进行 EPQ 测试和 WHERE CURRENT OF */
		fc_erm->curCtid = fc_tid;
	}

	/*
	 * 如果我们需要进行 EvalPlanQual 测试，就这样做。
	 */
	if (fc_epq_needed)
	{
		/* 初始化 EPQ 机制 */
		EvalPlanQualBegin(&fc_node->lr_epqstate);

		/*
		 * 要获取未锁定的源行，EPQ 逻辑需要访问被测试元组的垃圾列。
		 */
		EvalPlanQualSetSlot(&fc_node->lr_epqstate, fc_slot);

		/*
		 * 最后，我们可以重新评估元组。
		 */
		fc_slot = EvalPlanQualNext(&fc_node->lr_epqstate);
		if (TupIsNull(fc_slot))
		{
			/* 更新的元组未通过资格检查，因此忽略它并继续 */
			goto lnext;
		}
	}

	/* 已获得所有锁，因此返回当前元组 */
	return fc_slot;
}

/* ----------------------------------------------------------------
 *		ExecInitLockRows
 *
 *		这初始化 LockRows 节点状态结构和
 *		节点的子计划。
 * ----------------------------------------------------------------
 */
LockRowsState *
ExecInitLockRows(LockRows *fc_node, EState *fc_estate, int fc_eflags)
{
	LockRowsState *fc_lrstate;
	Plan	   *fc_outerPlan = outerPlan(fc_node);
	List	   *fc_epq_arowmarks;
	ListCell   *fc_lc;

	/* 检查不支持的标志 */
	Assert(!(fc_eflags & EXEC_FLAG_MARK));

	/*
	 * 创建状态结构
	 */
	fc_lrstate = makeNode(LockRowsState);
	fc_lrstate->ps.plan = (Plan *) fc_node;
	fc_lrstate->ps.state = fc_estate;
	fc_lrstate->ps.ExecProcNode = fc_ExecLockRows;

	/*
	 * 杂项初始化
	 *
	 * LockRows 节点从不调用 ExecQual 或 ExecProject，因此不需要
	 * ExprContext。
	 */

	/*
	 * 初始化结果类型。
	 */
	ExecInitResultTypeTL(&fc_lrstate->ps);

	/*
	 * 然后初始化外部计划
	 */
	outerPlanState(fc_lrstate) = ExecInitNode(fc_outerPlan, fc_estate, fc_eflags);

	/* 节点从外部计划返回未修改的插槽 */
	fc_lrstate->ps.resultopsset = true;
	fc_lrstate->ps.resultops = ExecGetResultSlotOps(outerPlanState(fc_lrstate),
												 &fc_lrstate->ps.resultopsfixed);

	/*
	 * LockRows 节点不进行投影，因此相应地初始化
	 * 此节点的投影信息。
	 */
	fc_lrstate->ps.ps_ProjInfo = NULL;

	/*
	 * 找到此节点负责的 ExecRowMark，并为它们构造 ExecAuxRowMarks。
	 * （InitPlan 应该已经构建了 ExecRowMarks 的全局列表。）
	 */
	fc_lrstate->lr_arowMarks = NIL;
	fc_epq_arowmarks = NIL;
	foreach(fc_lc, fc_node->rowMarks)
	{
		PlanRowMark *fc_rc = lfirst_node(PlanRowMark, fc_lc);
		ExecRowMark *fc_erm;
		ExecAuxRowMark *fc_aerm;

		/* 忽略“父”行标记；在运行时它们是无关的 */
		if (fc_rc->isParent)
			continue;

		/* 查找 ExecRowMark 并构建 ExecAuxRowMark */
		fc_erm = ExecFindRowMark(fc_estate, fc_rc->rti, false);
		fc_aerm = ExecBuildAuxRowMark(fc_erm, fc_outerPlan->targetlist);

		/*
		 * 只有锁定的行标记会进入我们自己的列表。非锁定标记被
		 * 移交给 EvalPlanQual 机制。这是因为我们不想在
		 * 实际需要进行 EPQ 重新检查之前去获取未锁定的行。
		 */
		if (RowMarkRequiresRowShareLock(fc_erm->markType))
			fc_lrstate->lr_arowMarks = lappend(fc_lrstate->lr_arowMarks, fc_aerm);
		else
			fc_epq_arowmarks = lappend(fc_epq_arowmarks, fc_aerm);
	}

	/* 现在我们有了设置 EPQ 状态所需的信息 */
	EvalPlanQualInit(&fc_lrstate->lr_epqstate, fc_estate,
					 fc_outerPlan, fc_epq_arowmarks, fc_node->epqParam);

	return fc_lrstate;
}

/* ----------------------------------------------------------------
 *		ExecEndLockRows
 *
 *		这关闭子计划并释放分配给此节点的资源。
 * ----------------------------------------------------------------
 */
void ExecEndLockRows(LockRowsState *fc_node)
{
	/* 我们可能已经关闭了 EPQ，但再调用一次也无妨 */
	EvalPlanQualEnd(&fc_node->lr_epqstate);
	ExecEndNode(outerPlanState(fc_node));
}


void ExecReScanLockRows(LockRowsState *fc_node)
{
	/*
	 * 如果子节点的chgParam不为空，则计划将由第一个ExecProcNode重新扫描。
	 */
	if (fc_node->ps.lefttree->chgParam == NULL)
		ExecReScan(fc_node->ps.lefttree);
}
